fix(webhooks): stop self-feeding inbound relay loop - #66
Merged
Conversation
A domain whose outbound target points back at its own inbound receiver fed itself forever: the receiver recorded each event, relayed it as `inbound.<type>`, and the relay re-entered the receiver, which read `inbound.<type>` off our own envelope and relayed `inbound.inbound.<type>`. The chain reset to bare `event` at 10 hops (past the 80-char event-type limit) and started over. moshcode.sh hit this and filled its whole 200-row event log with junk. Three layers, so no single topology can reproduce it: - relayEventType() applies the `inbound.` prefix at most once. - isSelfWebhookUrl() rejects targets aimed at our own /api/webhooks/*: skipped in fireDomainEvent, dead-lettered in deliverToEndpoint, and refused with an explanatory message when adding a domain target. - An x-moshcoding-hop counter caps relays at 3 hops, which bounds the same loop for hosts we can't recognize as ours (a custom domain proxied to this app). fireDomainEvent now also sends a user-agent, so a relayed event is identifiable in the inbound log instead of showing up as bare `Bun/x.y`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ralyodio
added a commit
that referenced
this pull request
Jul 28, 2026
First tagged release. Headline change: the inbound webhook relay loop fix (#66). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The dashboard's inbound event log for
moshcode.shwas full ofinbound.inbound.inbound.…evententries, all fromBun/1.3.14.Not a display bug.
moshcode.shhas an active outbound target pointing at its own inbound receiver (https://moshcoding.com/api/webhooks/moshcode.sh), so:inbound.<type>.type: "inbound.event"out of our own envelope.inbound.inbound.event— and so on, forever.The chain resets to bare
eventat 10 hops because the name exceeds the 80-char limit innormalizeInboundEventType, which returnsnull. TheBun/1.3.14user-agent was the tell:fireDomainEventwas the only sender not setting a UA, so the server was POSTing to itself.All 200 stored events for that domain (the per-domain cap) are loop junk.
The fix
Three independent layers, so no single topology reproduces it:
relayEventType()— applies theinbound.prefix at most once, so prefix chains can't grow regardless of routing.isSelfWebhookUrl()(lib/url-guard.ts) — targets aimed at our own/api/webhooks/*are skipped infireDomainEvent, dead-lettered indeliverToEndpoint, and refused with an explanatory message when added via the dashboard.x-moshcoding-hopcounter, capped atMAX_RELAY_HOPS = 3— bounds the same loop for hosts we can't statically recognize as ours (e.g. a custom domain proxied to this app).fireDomainEventnow also sets auser-agent, so relayed events are identifiable in the log.Trade-off
The self-target guard also blocks chaining domain A's outbound to domain B's receiver on moshcoding. That's a plausible-but-unused setup; the hop cap alone would have permitted it. Easy to relax if we ever want it.
Testing
bun test tests/→ 25 pass / 0 fail (was 22).bunx tsc --noEmitclean.New coverage: prefix applied once across 12 simulated hops, hop-counter parsing/clamping, and self-URL detection (including the
notmoshcoding.comnear-miss).Follow-up, not in this PR
domain_webhooksrow formoshcode.shis inert once this ships (deliveries to it are skipped), but is worth deleting..github/workflows/test.ymlskips whenpackage.jsonhas notestscript — which is the case here, so these tests don't run in CI.🤖 Generated with Claude Code